fix: deallocate failed prepare by server-side statement name - #2646
Open
istoolsfox wants to merge 1 commit into
Open
fix: deallocate failed prepare by server-side statement name#2646istoolsfox wants to merge 1 commit into
istoolsfox wants to merge 1 commit into
Conversation
When name == sql, Prepare creates the statement on the server under a digest name (stmt_<sha256>) while the client maps it under the SQL text. If the prepare failed after ParseComplete (e.g. statement_timeout between Parse and Describe), failedDescribeStatement stored the SQL text, so the deferred Deallocate sent Close with the SQL text as the statement name. That closes nothing: the stmt_<sha256> statement leaks and every retry of the same sql on this connection fails with 42P05 duplicate_prepared_statement, re-arming the failed cleanup each time for the lifetime of the connection. Store the server-side name (psName) instead. For the named path (name != sql) psName and psKey are identical, so behavior there is unchanged. Fixes jackc#2640
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a prepare fails after
ParseComplete(e.g. astatement_timeoutorpg_cancel_backendlanding between Parse and Describe), the statement has already been created on the server under the digest namestmt_<sha256>. The deferred cleanup introduced in 5.8.0 for #2223 storespsKey— the SQL text — instead ofpsName, soConn.Deallocatelooks it up inc.preparedStatements, finds nothing (a failed prepare was never stored there), and falls through to sending theCloseprotocol message with the full SQL text as the statement name. No statement by that name exists, closing one is not an error, so the cleanup "succeeds" while deallocating nothing.This is invisible through the
pgx.ConnAPI (callers passname != sqlthere), but it is the default path forstdlib.Conn.PrepareContext, which callsc.conn.Prepare(ctx, query, query)— as used by GORM withPrepareStmt: true. On such a connection the server-sidestmt_<digest>leaks, and every retry of the same SQL fails with42P05 duplicate_prepared_statement— which is itself aPrepareError, sofailedDescribeStatementgets set again and the next cleanup no-ops again, poisoning the connection for its remaining lifetime. That is precisely the failure class the #2223 fix was meant to close, still reachable through the digest path.Fix
Store
psName(the name the server actually knows) instead ofpsKey:For the named path (
name != sql),psName == psKey, so behavior there is unchanged. For the digest path, the deferredDeallocatenow sendsCloseforstmt_<digest>, which removes the leaked statement and lets the retry succeed. If the failure happened beforeParseComplete, no statement exists and theCloseis a harmless no-op, as before.Test
TestPrepareHandlesTimeoutBetweenParseAndDescribeWhenNameEqualsSQLmirrors the existing #2223 test but goes through the digest path (Prepare(ctx, sql, sql)), using thefaultyconnhook to induce the same deterministic timeout between Parse and Describe. It verifies the statement exists on the server under its digest name after the failed prepare, and that a retry of the same SQL succeeds.On the previous code the test fails exactly as reported:
With the fix it passes. Full
pgxroot package andstdlibtest suites pass against PostgreSQL 17.Fixes #2640